package models;

import static akka.pattern.Patterns.ask;
import static java.util.concurrent.TimeUnit.SECONDS;
import models.GameException;
import models.Hand;
import models.msgs.ActionRequest;
import models.msgs.AfterMoveInfo;
import models.msgs.CardInfo;
import models.msgs.CoinsInfo;
import models.msgs.Info;
import models.msgs.Join;
import models.msgs.MovePermission;
import models.msgs.Number;
import models.msgs.Quit;
import models.msgs.WinnerInfo;
import play.libs.Akka;
import play.mvc.WebSocket;
import scala.concurrent.Await;
import scala.concurrent.duration.Duration;
import akka.actor.ActorRef;
import akka.actor.Props;
import akka.actor.UntypedActor;

import com.fasterxml.jackson.databind.JsonNode;

public class Table extends UntypedActor {
    static int numberOfPlayers;
    static int startingCoins;
    static int smallBlind;
    static int bigBlind;
    static int maxBet;
    static int stake = 0;
    static int round = 1;
    static Deck2 deck;
    static int whoDealer;
    static int whoseTurn;
    private static int playerWithSmallBlind;
    
    static int moveCounter = 1; // liczba wykonanych ruchów w rundzie
    static int playersExchangedCardsCounter = 0;
    static int playersFoldedCounter = 0;
    static int playersInGameCounter;
    static int playersBeingInGameInActualRound;
    static boolean isBet = false;
    static int sitingPlayersCounter = 0;

    static ActorRef tableActor;
    static ActorRef[] playersActors;
    static Player[] players;

    
    public Table(int numberOfPlayers, int startingCoins, int smallBlind,
	    int bigBlind) {
	Table.numberOfPlayers = numberOfPlayers;
	Table.startingCoins = startingCoins;
	Table.smallBlind = smallBlind;
	Table.bigBlind = bigBlind;
	Table.tableActor = this.getSelf();
	Table.deck = new Deck2();
	Table.playersActors = new ActorRef[numberOfPlayers+1];
	Table.players = new Player[numberOfPlayers+1];
    }

    public static void join(final String name, WebSocket.In<JsonNode> in,
	    WebSocket.Out<JsonNode> out) throws Exception {

	// Send the Join message to the table
	String result = (String) Await.result(
		ask(tableActor, new Join(name), 1000),
		Duration.create(1, SECONDS));

	if ("OK".equals(result)) {

	    sitingPlayersCounter++;
	    ActorRef playerRef = Akka.system().actorOf(
		    Props.create(Human.class, name, in, out, tableActor,
			    sitingPlayersCounter));
	    playersActors[sitingPlayersCounter] = playerRef;
	    players[sitingPlayersCounter] = new Player(name,
		    sitingPlayersCounter, startingCoins, deck);
	    
	    String text = "my Actor has been added to Table";
	    notifyAll(new Info(text, name));
	    if (sitingPlayersCounter == numberOfPlayers) {
		whoDealer = Croupier.drawDealer(numberOfPlayers);
		try {
		    setPlayersWithBlinds();
		} catch (GameException e) {
		}
		whoseTurn = Croupier.whichPlayerNext(whoseTurn, numberOfPlayers);
		playersBeingInGameInActualRound = playersInGameCounter;
		for (int i = 1; i < playersActors.length; i++) 
		    notifyOne(new CardInfo(players[i].getStringHand()), i);
		enableButtons(whoseTurn, isBet, false);
	    }
	}
    }

    public static void quit(final String username) throws Exception {

	// to do
    }

    public void onReceive(Object message) throws Exception {

	if (message instanceof Join) {

	    Join join = (Join) message;
	    if (sitingPlayersCounter == numberOfPlayers) {
		getSender().tell("NO", getSelf());
	    }
	    else if (false) {
		// if username is already taken do sth
	    } else {
		getSender().tell("OK", getSelf());
	    }
	} else if (message instanceof ActionRequest) {
	    String type = ((ActionRequest)message).getType();
	    switch (type) {
	    case "bet":
		try {
		    updateBet(((ActionRequest)message).getNumber(), ((ActionRequest)message).getValue());
		} catch (GameException e) {
		    updateFold(((ActionRequest)message).getNumber(), true);
		}
		break;
	    case "call":
		updateCall(((ActionRequest)message).getNumber());
		break;
	    case "check":
		nextPlayerMove();
		break;
	    case "raise":
		try {
		    updateBet(((ActionRequest)message).getNumber(), ((ActionRequest)message).getValue());
		} catch (GameException e) {
		    updateFold(((ActionRequest)message).getNumber(), true);
		}
		break;
	    case "fold":
		updateFold(((ActionRequest)message).getNumber(), false);
		break;
	    case "exchange":
		try {
		    exchangeCards(((ActionRequest)message).getNumber(), ((ActionRequest)message).getValue());
		} catch (GameException e) {
		    exchangeCards(((ActionRequest)message).getNumber(), 0);
		}
		break;
			
	    }
	    
		
	    

	} else if (message instanceof Number) {
	    int nr = ((Number) message).getNumber();
	    String name = ((Number) message).getName();
	    getSender().tell(Deck.giveCard(nr), getSelf());
	    String text = "I've sent    " + Deck.giveCard(nr).getRank() + " "
		    + Deck.giveCard(nr).getColor() + "    to " + name;
	    notifyAll(new Info(text, "Table"));
	} else if (message instanceof Info) {
	    String text = ((Info) message).getText();
	    String name = ((Info) message).getName();

	    notifyAll(new Info(text, name));
	} else if (message instanceof Quit) {

	    String name = ((Quit) message).getName();
	    //playersActors.remove(name);
	    String text = "has left the game";
	    notifyAll(new Info(text, name));

	} else {
	    unhandled(message);
	}

    }

    static public void notifyAll(Object msg) {
	for (int i = 1 ; i <=sitingPlayersCounter ; i++) {
	    playersActors[i].tell(msg, tableActor);
	}
    }

    static public void notifyOne(Object msg,int playerNumber) {
	playersActors[playerNumber].tell(msg, tableActor);

    }

    public static boolean isNameUsed(String name) {
	for (int i = 1; i <= sitingPlayersCounter; i++) {
	    if (name.equals(players[i].getName()))
		return true;
	}
	return false;
    }
    
    public static void setPlayersWithBlinds() throws GameException {
	playerWithSmallBlind = Croupier.whichPlayerNext2(whoDealer, players,
		false);
	while (players[playerWithSmallBlind].getPlayersCoins() < smallBlind) {
	    updateFold(playerWithSmallBlind, true);
	    playerWithSmallBlind = Croupier.whichPlayerNext2(whoDealer,
		    players, false);
	}
	playerWithSmallBlind = Croupier.whichPlayerNext2(whoDealer, players,
		false);
	players[playerWithSmallBlind].setBet(smallBlind);
	notifyAll(new Info("I have a small blind (" + Integer.toString(smallBlind)+")",players[playerWithSmallBlind].getName()));
	updateCoins(players[playerWithSmallBlind], 0 - smallBlind);
	int playerWithBigBlind = Croupier.whichPlayerNext2(
		playerWithSmallBlind, players, false);
	while (players[playerWithBigBlind].getPlayersCoins() < bigBlind) {
	    updateFold(playerWithBigBlind, true);
	    playerWithBigBlind = Croupier.whichPlayerNext2(
		    playerWithSmallBlind, players, false);
	}
	players[playerWithBigBlind].setBet(bigBlind);
	maxBet = bigBlind;
	whoseTurn = playerWithBigBlind;
	notifyAll(new Info("I have a big blind (" + Integer.toString(bigBlind)+")",players[playerWithBigBlind].getName()));
	updateCoins(players[playerWithBigBlind], 0 - bigBlind);
	updateStake(smallBlind + bigBlind);
    }
    
    public static void updateFold(int playerNr, boolean isPermament) {
	if (isPermament)
	    players[playerNr].setStatus(Player.PLAYER_PERMAMENTLY_OUT_OF_GAME);
	else
	    players[playerNr].setStatus(Player.PLAYER_OUT_OF_GAME);
	notifyAll(new Info("I folded",players[playerNr].getName()));
	playersInGameCounter--;
	playersFoldedCounter++;
	if (whoseTurn == playerNr)
	    nextPlayerMove();
	else {
	   playersBeingInGameInActualRound--;
	    if (playersBeingInGameInActualRound < 2)
		nextPlayerMove();
	}

    }
    
    public static void updateCoins(Player player, int change) {
	int newCoins = player.getPlayersCoins() + change;
	player.setPlayersCoins(newCoins);
	notifyAll(new CoinsInfo(player.getName(), newCoins));
    }
    
    public static void enableButtons(int playerNr, boolean isBet, boolean addCheck) {
	int bet = players[playerNr].getBet();
	int coins = players[playerNr].getPlayersCoins();
	String possibleMoves = Croupier
		.possibleMoves(bet, coins, isBet, maxBet, round);
	if (addCheck)
	    possibleMoves = "H" + possibleMoves;
	notifyAll(new Info("It is my turn!", players[playerNr].getName()));
	notifyOne(new MovePermission(possibleMoves), playerNr);
    }
    
    public static void nextPlayerMove() {
	
	if (playersFoldedCounter < (numberOfPlayers - 1)) {
	    if (playersBeingInGameInActualRound > 1) { // chociaż dwóch graczy
						       // nie dalo fold ani
						       // all-in
		whoseTurn = Croupier.whichPlayerNext(whoseTurn, numberOfPlayers);
		// gdy wszyscy wymienili karty
		if (playersExchangedCardsCounter == (numberOfPlayers - playersFoldedCounter)) {
		    updateRound();
		}
		// runda pierwsza i nikt przed osobą z dużą ciemną nie przebił
		// duzej ciemnej
		if (moveCounter == numberOfPlayers - 1 && round == 1
			&& Croupier.havePlayersTheSameBets(players)) {
		    enableButtons(whoseTurn, isBet, true); // dodatkowo może dać
							   // check
		    moveCounter++;
		}// POCZATEK WYMIANY wszyscy gracze wykonali mogli wykonać ruch
		 // i gracze w grze mają te same zaklady
		else if (moveCounter >= playersBeingInGameInActualRound
			&& Croupier.havePlayersTheSameBets(players)) {
		    if (round < 4) {
			if (playersExchangedCardsCounter == 0)
			    playersBeingInGameInActualRound = playersInGameCounter;
			while (players[whoseTurn].getStatus() <= Player.PLAYER_OUT_OF_GAME) {
			    whoseTurn = Croupier.whichPlayerNext(whoseTurn,
				    numberOfPlayers);
			}
			notifyOne(new MovePermission("E"), whoseTurn);
			playersExchangedCardsCounter++;
		    } else { // KONIEC GRY po 4 rundach- przynajmniej dwóch
			     // graczy nie
			     // dało fold ani all-in
			displayWinner();
			givePlayerTheirPrize();
			prepareNewDeal();
		    }
		} else { // Kontynuacja ruchów- rózne zakłady lub nie wszyscy
			 // mogli
			 // wykonać ruch
		    while (players[whoseTurn].getStatus() != Player.PLAYER_IN_GAME) {
			whoseTurn = Croupier.whichPlayerNext(whoseTurn, numberOfPlayers);
		    }
		    enableButtons(whoseTurn, isBet, false);
		    moveCounter++;
		}
	    }// jest tylko jeden gracz, który nie dał fold ani all-in, i chociaż
	     // jeden z all-in
	    else {
		whoseTurn = Croupier.whichPlayerNext(whoseTurn, numberOfPlayers);
		// wszyscy wymienili karty
		if (playersExchangedCardsCounter == (numberOfPlayers - playersFoldedCounter)) {
		    updateRound();
		}
		// trwa wymiana kart
		if (round < 4) {
		   while (players[whoseTurn].getStatus() <= Player.PLAYER_OUT_OF_GAME)
			whoseTurn = Croupier.whichPlayerNext(whoseTurn, numberOfPlayers);
		    notifyOne(new MovePermission("E"), whoseTurn);
		    playersExchangedCardsCounter++;
		} else {// KONIEC GRY
		    displayWinner();
		    givePlayerTheirPrize();
		    prepareNewDeal();
		}
	    }
	} else {// Koniec GRY został tylko jeden gracz w grze(zwyciezca), reszta
		// dała
		// fold
	    displayWinner();
	    givePlayerTheirPrize();
	    prepareNewDeal();
	   
	}
    }
    
    public static void displayWinner() {
	//TO DO class WinnerInfo may look the way you want
	String message = "";
	for (int i = 1; i < players.length; i++) {
	    if (players[i].getStatus() <= Player.PLAYER_OUT_OF_GAME) {
		message += players[i].getName() + "FOLDED**"; // must be 8
							      // characters
	    } else {
		message += players[i].getName()
			+ players[i].getPlayersHand().getString();
	    }
	}
	message += getWinners();
	WinnerInfo winInfo = new WinnerInfo(message);
	notifyAll(winInfo);
	
    }
    
    private static String getWinners() {
	String winners = "";
	int[][] Hands = new int[players.length][];

	for (int i = 1; i < players.length; i++) {
	    Hands[i] = Hand.getReducedHand(players[i].getPlayersHand()
		    .getCards());
	}
	int[] bestHand = { -1, -1, -1, -1 };
	for (int i = 1; i < players.length; i++) {
	    if (players[i].getStatus() > Player.PLAYER_OUT_OF_GAME) {
		if (bestHand[0] != -1)
		    bestHand = Hand.giveBetterHand(bestHand, Hands[i]);
		else
		    bestHand = Hands[i];
	    }
	}
	for (int i = 1; i < players.length; i++) {
	    if (players[i].getStatus() <= Player.PLAYER_OUT_OF_GAME) {
		winners += "L";
	    } else {
		if (!(bestHand.equals(Hands[i]))) {
		    if (!(Hand.giveBetterHand(Hands[i], bestHand).equals(Hand
			    .giveBetterHand(bestHand, Hands[i])))) {
			winners += "W";
		    } else
			winners += "L";
		} else
		    winners += "W";
	    }
	}
	return winners;

    }
    
    public static void givePlayerTheirPrize() {
	String winnersString = getWinners();
	int numberOfWinners = 0;
	for (int i = 0; i < winnersString.length(); i++) {
	    if (Character.toString(winnersString.charAt(i)).equals("W")) {
		numberOfWinners++;
	    }
	}
	if (numberOfWinners == 1) { // Only one winner. Bank doesn't take a
				    // prize
	    for (int i = 0; i < winnersString.length(); i++) {
		if (Character.toString(winnersString.charAt(i)).equals("W")) {
		    if (players[i + 1].getStatus() != Player.PLAYER_PLAYED_ALLIN) { // regular
										    // winner
										    // (NOT
										    // ALL
										    // IN)
			updateCoins(players[i + 1], stake);
		    } else { // player[i] played ALL IN
			int prize = 0;
			int allInBet = players[i + 1].getBet();
			for (int j = 1; j < players.length; j++) {
			    if (allInBet <= players[j].getBet())
				prize += allInBet;
			    else
				prize += players[j].getBet();
			}
			updateCoins(players[i + 1], prize);
		    }
		}
	    }
	}

    }
    
    public void updateBet(int playerNr, int bet) throws GameException {
	isBet = true;
	if (bet <= 0 && bet > players[playerNr].getPlayersCoins())
		throw new GameException("You send incorrect bet");
	int sum = bet + players[playerNr].getBet();
	if (sum > maxBet)
	maxBet = sum;
	players[playerNr].setBet(sum);
	players[playerNr].setPlayersCoins(players[playerNr].getPlayersCoins() - bet);
	updateStake(bet);
	notifyAll(new AfterMoveInfo(maxBet, stake, players[playerNr].getPlayersCoins(), sum, players[playerNr].getName()));
	nextPlayerMove();
	

    }
    
    public void updateCall(int playerNr) {
	isBet = true;
	int change = maxBet - players[playerNr].getBet();
	players[playerNr].setBet(maxBet);
	players[playerNr].setPlayersCoins(players[playerNr].getPlayersCoins() - change);
	updateStake(change);
	notifyAll(new AfterMoveInfo(maxBet, stake, players[playerNr].getPlayersCoins(), maxBet, players[playerNr].getName()));
	    nextPlayerMove();
	nextPlayerMove();
    }
    
    public static void updateStake(int betChange) {
	stake = stake + betChange;
    }
    
    public static void prepareNewDeal() {
	int playersPermamentlyFoldedCounter = 0;
	for (int i = 1; i < players.length; i++) {// chyba trzeba zwrócić karty
						  // gracza odłaczonego
	    players[i].getPlayersHand().discardAllCards(deck);
	    players[i].setBet(0);
	    if (players[i].getStatus() != Player.PLAYER_PERMAMENTLY_OUT_OF_GAME
		    && players[i].getPlayersCoins() > 0) {
		players[i].setStatus(Player.PLAYER_IN_GAME);
	    } else {
		players[i].setStatus(Player.PLAYER_PERMAMENTLY_OUT_OF_GAME);
		players[i].setBet(0);
		playersPermamentlyFoldedCounter++;
		//deletePlayer(i);
	    }
	}
	deck.shuffle();

	if ((numberOfPlayers - playersPermamentlyFoldedCounter) < 2) {
	    int i = 1;
	    while (players[i].getStatus() != Player.PLAYER_IN_GAME) {
		i++;
	    }
	    for (int j = 1; j < players.length; j++) {
		notifyAll(new Info(players[i].getName()+" won.\nThe game is over", "Table"));
	    }
	} else {
	    for (int i = 1; i < players.length; i++) {
		if (players[i].getStatus() != Player.PLAYER_PERMAMENTLY_OUT_OF_GAME) {
		    players[i].getPlayersHand().drawAllCards(deck, i);
		    /*for (int j = 1; j < players.length; j++) {
			if (playersActors[j].isWorking())
			    playersActors[j].displayBet("0",
				    players[i].getName());
		    }*/
		}
	    }
	    round = 1;
	    stake = 0;
	    isBet = false;
	    playersFoldedCounter = playersPermamentlyFoldedCounter;
	    playersInGameCounter = numberOfPlayers - playersPermamentlyFoldedCounter;
	    playersExchangedCardsCounter = 0;
	    playersBeingInGameInActualRound = playersInGameCounter;
	    try {
		whoDealer = Croupier
			.whichPlayerNext2(whoDealer, players, false);
	    } catch (GameException e) {
	    }
	    notifyAll(new Info("Round " + Integer.toString(round), "Table"));
	}
    }
    
    public static void updateRound() {
	round++;
	moveCounter = 0;
	isBet = false;
	playersExchangedCardsCounter = 0;
	playersBeingInGameInActualRound = playersInGameCounter;
	whoseTurn = playerWithSmallBlind;
	notifyAll(new Info("Round " + Integer.toString(round), "Table"));
    }

    public void exchangeCards(int playerNr, int cardSet)
	    throws GameException {
	if (cardSet > 15 || cardSet < 0)
	throw new GameException(
		"You tried exchange more cards than you had had");
	if (cardSet > 0) {
	int exchangedCardsCounter = 0;
	String binaryCardSet = Integer.toBinaryString(cardSet);
	for (int i = 0; i < binaryCardSet.length(); i++) {
	    if (binaryCardSet.charAt(i) == '1')
		exchangedCardsCounter++;
	}
	players[playerNr].exchangeCards(deck, cardSet);
	notifyOne(new CardInfo(players[playerNr].getStringHand()), playerNr);
	notifyAll(new Info(players[playerNr].getName()+ "exchanged " + exchangedCardsCounter+ " cards", "Table"));
	}
	nextPlayerMove();
    }
}
